前言
iOS中定时器的使用还是很常见的。那么iOS中有几种定时器,平时又怎么使用呢?
NSTimer
在详解RunLoop之源码分析一文中,简单描述了NStimer和RunLoop的关系
默认情况下,NSTimer计时器,会被UIScrollView 打断,会影响计时器的使用。原因就是滚动时候,RunLoop切换到了UITrackingRunLoopMode模式下,但计时器在NSDefaultRunLoopMode下,所以就停止了。解决办法就是设置NSRunLoopCommonModes。特别注意的是:NSRunLoopCommonModes并不是一个真的模式,它只是一个标记.如果设置了NSRunLoopCommonModes timer能在_commonModes数组中存放的模式下工作。
使用
下面两个定时器的使用是等价的
1 | self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES]; |
上面两个方法的等价的,区别是第二张,需要自己手动加到RunLoop中,否则不生效。苹果中关于NSTimer的源码是不开源的,但是我们可以参考GNUstep源码地址中的源码
scheduledTimerWithTimeInterval
1 | + (NSTimer*) scheduledTimerWithTimeInterval: (NSTimeInterval)ti |
和 timerWithTimeInterval
1 | + (NSTimer*) timerWithTimeInterval: (NSTimeInterval)ti |
从上面的源码可知,这两种方式,调用的定时器是一样的,但是第一种会自动添加到RunLoop中,不需要我们来处理了。
但是上面两种都会导致循环引用。原因也很好理解,控制器持有定时器,定时器的target指向当前控制器,所以就循环引用了。
解决循环引用
__weak 不能解除循环引用
1 | __weak typeof(self) weakSelf = self; |
__weak
和block
能解除循环引用
1 |
|
上面的代码中是可以解除循环引用的,然后真正起作用的,是block。和timer并没有加什么关系,详细可以看深入理解iOS的block一文,有详细说明。
那么问题来了,要怎么解除循环引用呢?
invalidate解除循环引用
1 | - (void)viewDidLoad { |
上面代码中,在控制器即将消失的时候,调用[self.timer invalidate]
;能解除循环引用。但是,在开发中一般不这样用。因为,页面跳转了就会调用viewWillDisappear
,然后有时候业务逻辑很复杂,此时并不想取消定时器。更多的时候想让定时器和控制器的生命绑定在一起,那我们可否这么写呢
1 | - (void)viewDidLoad { |
答案是不行的,因为已经循环引用了,在dealloc
里面调用[self.timer invalidate]
,那这代码永远不会执行。
NSProxy
解除循环引用
NSProxy
是不继承自NSObject
的。专门用来做这个事的。
NSProxy
的效率很高,因为不经过Runtime
的,消息发送,消息动态解析,去缓存中查找等流程,直接通过消息转发。关于Runtime
的详细分析,可以参考详解iOS中的Runtime
API如下
1 | @interface NSProxy <NSObject> { |
例如isKindOfClass和isMemberOfClass等等,都是直接走消息转发
1 | - (BOOL) isKindOfClass: (Class)aClass |
新建类YZProxy
继承自NSProxy
,具体代码如下
1 |
|
CADisplayLink
除了NSTimer之外,还可以使用CADisplayLink定时器
1 | - (void)viewDidLoad { |
注意点
CADisplayLink
和NSTimer
一样也会导致循环引用,解决办法和前面的NSTimer
一样。区别就是CADisplayLink
并没有类似NSTimer
中的block
方法:+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
dispatch_source_t
前面说了定时器NSTimer
和CADisplayLink
,但是,他们都是和RunLoop
相关的,所以,从详解RunLoop之源码分析中我们知道,我们的计时器设置是每1秒执行一次,假设RunLoop
执行完一圈耗时0.3秒,当执行0.8秒的时候,开始下一圈的RunLoop
,当执行完之后,已经是1.1秒了。所以,这两种计时器不够精确。当然了,实际上RunLoop每一圈的耗时远远小于0.3,这里只是为了方便说明问题而举例。
1 | @interface ViewController () |
两种设置时间方式
主要注意的是,dispatch_source_set_timer
需要一个参数dispatch_time_t,而dispatch_time_t
的创建有两种
- dispatch_time(dispatch_time_t when, int64_t delta)
1 | 第一个参数是从什么时间开始,一般直接传 DISPATCH_TIME_NOW; 表示从现在开始 |
dispatch_time_t dispatch_time(dispatch_time_t when, int64_t delta); dispatch_walltime(const struct timespec *_Nullable when, int64_t delta)
1 | 第一个参数是一个结构体, 创建的是一个绝对的时间点,比如 2016年10月10日8点30分30秒, 如果你不需要自某一个特定的时刻开始,可以传 NUll,表示自动获取当前时区的当前时间作为开始时刻,。 |
这两种方式的区别是:
例如: 从现在开始,1小时之后是触发某个事件
使用第一个函数创建的是一个相对的时间,第一个参数开始时间参考的是当前系统的时钟,当 device 进入休眠之后,系统的时钟也会进入休眠状态, 第一个函数同样被挂起; 假如 device 在第一个函数开始执行后10分钟进入了休眠状态,那么这个函数同时也会停止执行,当你再次唤醒 device 之后,该函数同时被唤醒,但是事件的触发就变成了从唤醒 device 的时刻开始,1小时之后触发某个事件
而第二个函数则不同,他创建的是一个绝对的时间点,一旦创建就表示从这个时间点开始,1小时之后触发事件,假如 device 休眠了10分钟,当再次唤醒 device 的时候,计算时间间隔的时间起点还是 开始时就设置的那个时间点, 而不会受到 device 是否进入休眠影响
这种方式可以更加精确地使用定时器,因为是直接跟内核挂钩的,跟RunLoop
没有关系,所以也不会有常见的RunLoop
模式的改变而导致定时器的暂停等问题。如果我们需要对定时器的精度要求很高的话,可以考虑dispatch_source_t
去实现